package service

import (
	"context"
	"strings"

	"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
	hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
	"github.com/fleetdm/fleet/v4/server/fleet"
	"github.com/fleetdm/fleet/v4/server/ptr"
	"github.com/go-kit/log/level"
	"github.com/google/uuid"
)

func (svc *Service) GetOrbitSetupExperienceStatus(ctx context.Context, orbitNodeKey string, forceRelease bool, resetFailedSetupSteps bool) (*fleet.SetupExperienceStatusPayload, error) {
	// this is not a user-authenticated endpoint
	svc.authz.SkipAuthorization(ctx)

	host, ok := hostctx.FromContext(ctx)
	if !ok {
		err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
		return nil, err
	}

	if fleet.IsLinux(host.Platform) || host.Platform == "windows" {
		// Windows and Linux setup experience only have software.
		status, err := svc.getHostSetupExperienceStatus(ctx, host)
		if err != nil {
			return nil, ctxerr.Wrap(ctx, err, "get host setup experience status")
		}
		return &fleet.SetupExperienceStatusPayload{
			Software: status.Software,
		}, nil
	}

	appCfg, err := svc.ds.AppConfig(ctx)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "getting app config")
	}

	// get the status of the bootstrap package deployment
	bootstrapPkg, err := svc.ds.GetHostMDMMacOSSetup(ctx, host.ID)
	if err != nil && !fleet.IsNotFound(err) {
		return nil, ctxerr.Wrap(ctx, err, "get bootstrap package status")
	}

	// NOTE: bootstrapPkg can be nil if there was none to install.
	var bootstrapPkgResult *fleet.SetupExperienceBootstrapPackageResult
	if bootstrapPkg != nil {
		bootstrapPkgResult = &fleet.SetupExperienceBootstrapPackageResult{
			Name:   bootstrapPkg.BootstrapPackageName,
			Status: bootstrapPkg.BootstrapPackageStatus,
		}
	}

	// get the status of the configuration profiles
	cfgProfs, err := svc.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "get configuration profiles status")
	}
	var cfgProfResults []*fleet.SetupExperienceConfigurationProfileResult
	for _, prof := range cfgProfs {
		// NOTE: DDM profiles (declarations) are ignored because while a device is
		// awaiting to be released, it cannot process a DDM session (at least
		// that's what we noticed during testing).
		if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
			continue
		}

		// NOTE: user-scoped profiles are ignored because they are not sent by Fleet
		// until after the device is released - there is no user-channel available
		// on the host until after the release, and after the user actually created
		// the user account.
		if prof.Scope == fleet.PayloadScopeUser {
			continue
		}

		status := fleet.MDMDeliveryPending
		if prof.Status != nil {
			status = *prof.Status
		}
		cfgProfResults = append(cfgProfResults, &fleet.SetupExperienceConfigurationProfileResult{
			ProfileUUID: prof.ProfileUUID,
			Name:        prof.Name,
			Status:      status,
		})
	}

	profilesMissingInstallation, err := svc.ds.ListMDMAppleProfilesToInstall(ctx, host.UUID)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "listing apple config profiles to install")
	}
	profilesMissingInstallation = fleet.FilterOutUserScopedProfiles(profilesMissingInstallation)
	if host.Platform != "darwin" {
		profilesMissingInstallation = fleet.FilterMacOSOnlyProfilesFromIOSIPadOS(profilesMissingInstallation)
	}
	if len(profilesMissingInstallation) > 0 {
		for _, prof := range profilesMissingInstallation {
			cfgProfResults = append(cfgProfResults, &fleet.SetupExperienceConfigurationProfileResult{
				ProfileUUID: prof.ProfileUUID,
				Name:        prof.ProfileName,
				Status:      fleet.MDMDeliveryPending, // Default to pending as it's not installed yet.
			})
		}
	}

	// AccountConfiguration covers the (optional) command to setup SSO.
	adminTeamFilter := fleet.TeamFilter{
		User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
	}
	acctCmds, err := svc.ds.ListMDMCommands(ctx, adminTeamFilter, &fleet.MDMCommandListOptions{
		Filters: fleet.MDMCommandFilters{
			HostIdentifier: host.UUID,
			RequestType:    "AccountConfiguration",
		},
	})
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "list AccountConfiguration commands")
	}

	var acctCfgResult *fleet.SetupExperienceAccountConfigurationResult
	if len(acctCmds) > 0 {
		// there may be more than one if e.g. the worker job that sends them had to
		// retry, but they would all be processed anyway so we can only care about
		// the first one.
		acctCfgResult = &fleet.SetupExperienceAccountConfigurationResult{
			CommandUUID: acctCmds[0].CommandUUID,
			Status:      acctCmds[0].Status,
		}
	}

	// get status of software installs and script execution
	res, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "listing setup experience results")
	}

	// Check if "require all software" is configured for the host's team.
	requireAllSoftware, err := svc.IsAllSetupExperienceSoftwareRequired(ctx, host)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "checking if all software is required")
	}

	hasFailedSoftwareInstall := false
	for _, r := range res {
		if r.IsForSoftware() && r.Status == fleet.SetupExperienceStatusFailure {
			hasFailedSoftwareInstall = true
			break
		}
	}
	// If we have a failed software install,
	// AND "require all software" is configured for the host's team,
	// AND the resetFailedSetupSteps flag is set,
	// then re-enqueue any cancelled setup experience steps.
	if hasFailedSoftwareInstall {
		if resetFailedSetupSteps {
			teamID := uint(0)
			if host.TeamID != nil {
				teamID = *host.TeamID
			}
			// If so, call the enqueue function with a flag to retain successful steps.
			if requireAllSoftware {
				level.Info(svc.logger).Log("msg", "re-enqueueing cancelled setup experience steps after a previous software install failure", "host_uuid", host.UUID)
				platform := host.PlatformLike
				if platform == "" {
					platform = host.Platform
				}
				_, err := svc.ds.ResetSetupExperienceItemsAfterFailure(ctx, platform, host.UUID, teamID)
				if err != nil {
					return nil, ctxerr.Wrap(ctx, err, "re-enqueueing cancelled setup experience steps after a previous software install failure")
				}
				// Re-fetch the setup experience results after re-enqueuing.
				res, err = svc.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
				if err != nil {
					return nil, ctxerr.Wrap(ctx, err, "listing setup experience results")
				}
			}
		}
	}

	err = svc.failCancelledSetupExperienceInstalls(ctx, host.ID, host.UUID, host.DisplayName(), res)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "failing cancelled setup experience installs")
	}

	payload := &fleet.SetupExperienceStatusPayload{
		BootstrapPackage:      bootstrapPkgResult,
		ConfigurationProfiles: cfgProfResults,
		AccountConfiguration:  acctCfgResult,
		Software:              make([]*fleet.SetupExperienceStatusResult, 0),
		OrgLogoURL:            appCfg.OrgInfo.OrgLogoURLLightBackground,
		RequireAllSoftware:    requireAllSoftware,
	}
	for _, r := range res {
		if r.IsForScript() {
			payload.Script = r
		}

		if r.IsForSoftware() {
			payload.Software = append(payload.Software, r)
		}
	}

	// If we have failed software, and all software is required,
	// we can go ahead and return now.
	if hasFailedSoftwareInstall && requireAllSoftware {
		return payload, nil
	}

	if forceRelease || isDeviceReadyForRelease(payload) {
		manual, err := isDeviceReleasedManually(ctx, svc.ds, host)
		if err != nil {
			return nil, ctxerr.Wrap(ctx, err, "check if device is released manually")
		}
		if manual {
			return payload, nil
		}

		// otherwise the device is not released manually, proceed with automatic
		// release
		if forceRelease {
			level.Warn(svc.logger).Log("msg", "force-releasing device, DEP enrollment commands, profiles, software installs and script execution may not have all completed", "host_uuid", host.UUID)
		} else {
			level.Info(svc.logger).Log("msg", "releasing device, all DEP enrollment commands, profiles, software installs and script execution have completed", "host_uuid", host.UUID)
		}

		// Host will be marked as no longer "awaiting configuration" in the command handler
		if err := svc.mdmAppleCommander.DeviceConfigured(ctx, host.UUID, uuid.NewString()); err != nil {
			return nil, ctxerr.Wrap(ctx, err, "failed to enqueue DeviceConfigured command")
		}
	}

	_, err = svc.SetupExperienceNextStep(ctx, host)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "getting next step for host setup experience")
	}

	return payload, nil
}

func (svc *Service) failCancelledSetupExperienceInstalls(
	ctx context.Context,
	hostID uint,
	hostUUID string,
	hostDisplayName string,
	results []*fleet.SetupExperienceStatusResult,
) error {
	for _, r := range results {
		if r.Status != fleet.SetupExperienceStatusCancelled {
			continue
		}
		r.Status = fleet.SetupExperienceStatusFailure
		level.Info(svc.logger).Log("msg", "marking setup experience software as failed due to cancellation", "host_uuid", hostUUID, "software_name", r.Name)
		err := svc.ds.UpdateSetupExperienceStatusResult(ctx, r)
		if err != nil {
			return ctxerr.Wrap(ctx, err, "failing cancelled setup experience software install")
		}
		// TODO -- support recording activity for failed VPP apps as well.
		// https://github.com/fleetdm/fleet/issues/34288
		if r.IsForSoftwarePackage() {
			softwarePackage := ""
			var source *string
			installerMeta, err := svc.ds.GetSoftwareInstallerMetadataByID(ctx, *r.SoftwareInstallerID)
			if err != nil && !fleet.IsNotFound(err) {
				return ctxerr.Wrap(ctx, err, "getting software installer metadata for cancelled setup experience software install")
			}
			if installerMeta != nil {
				softwarePackage = installerMeta.Name
				// Get the software title to retrieve the source
				if installerMeta.TitleID != nil {
					title, err := svc.ds.SoftwareTitleByID(ctx, *installerMeta.TitleID, nil, fleet.TeamFilter{})
					if err != nil && !fleet.IsNotFound(err) {
						return ctxerr.Wrap(ctx, err, "getting software title for cancelled setup experience software install")
					}
					if title != nil {
						source = &title.Source
					}
				}
			}
			activity := fleet.ActivityTypeInstalledSoftware{
				HostID:              hostID,
				HostDisplayName:     hostDisplayName,
				SoftwareTitle:       r.Name,
				SoftwarePackage:     softwarePackage,
				InstallUUID:         *r.HostSoftwareInstallsExecutionID,
				Status:              "failed",
				SelfService:         false,
				Source:              source,
				FromSetupExperience: true,
			}
			err = svc.NewActivity(ctx, nil, activity)
			if err != nil {
				return ctxerr.Wrap(ctx, err, "creating activity for cancelled setup experience software install")
			}
		}
		continue
	}
	return nil
}

func isDeviceReleasedManually(ctx context.Context, ds fleet.Datastore, host *fleet.Host) (bool, error) {
	var manualRelease bool
	if host.TeamID == nil {
		ac, err := ds.AppConfig(ctx)
		if err != nil {
			return false, ctxerr.Wrap(ctx, err, "get AppConfig to read enable_release_device_manually")
		}
		manualRelease = ac.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
	} else {
		tm, err := ds.TeamLite(ctx, *host.TeamID)
		if err != nil {
			return false, ctxerr.Wrap(ctx, err, "get Team to read enable_release_device_manually")
		}
		manualRelease = tm.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
	}
	return manualRelease, nil
}

func isDeviceReadyForRelease(payload *fleet.SetupExperienceStatusPayload) bool {
	// default to "do release" and return false as soon as we find a reason not
	// to.

	if payload.BootstrapPackage != nil {
		if payload.BootstrapPackage.Status != fleet.MDMBootstrapPackageFailed &&
			payload.BootstrapPackage.Status != fleet.MDMBootstrapPackageInstalled {
			// bootstrap package is still pending, not ready for release
			return false
		}
	}

	if payload.AccountConfiguration != nil {
		if payload.AccountConfiguration.Status != fleet.MDMAppleStatusAcknowledged &&
			payload.AccountConfiguration.Status != fleet.MDMAppleStatusError &&
			payload.AccountConfiguration.Status != fleet.MDMAppleStatusCommandFormatError {
			// account configuration command is still pending, not ready for release
			return false
		}
	}

	for _, prof := range payload.ConfigurationProfiles {
		if prof.Status != fleet.MDMDeliveryFailed &&
			prof.Status != fleet.MDMDeliveryVerifying &&
			prof.Status != fleet.MDMDeliveryVerified {
			// profile is still pending, not ready for release
			return false
		}
	}

	for _, sw := range payload.Software {
		if sw.Status != fleet.SetupExperienceStatusFailure &&
			sw.Status != fleet.SetupExperienceStatusSuccess {
			// software is still pending, not ready for release
			return false
		}
	}

	if payload.Script != nil {
		if payload.Script.Status != fleet.SetupExperienceStatusFailure &&
			payload.Script.Status != fleet.SetupExperienceStatusSuccess {
			// script is still pending, not ready for release
			return false
		}
	}

	return true
}

func (svc *Service) SetupExperienceInit(ctx context.Context) (*fleet.SetupExperienceInitResult, error) {
	// This is an orbit endpoint, not a user-authenticated endpoint.
	svc.authz.SkipAuthorization(ctx)

	// NOTE: currently, Android does not go through the "init" setup experience flow as it
	// doesn't support any on-device UI (such as the screen showing setup progress) nor any
	// ordering of installs - all software to install is provided as part of the Android policy
	// when the host enrolls in Fleet.
	// See https://github.com/fleetdm/fleet/issues/33761#issuecomment-3548996114

	host, ok := hostctx.FromContext(ctx)
	if !ok {
		return nil, ctxerr.New(ctx, "internal error: missing host from request context")
	}

	// teamID for EnqueueSetupExperienceItems should be 0 for "No team" hosts.
	var teamID uint
	if host.TeamID != nil {
		teamID = *host.TeamID
	}

	hostUUID, err := fleet.HostUUIDForSetupExperience(host)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "failed to get host's UUID for the setup experience")
	}

	enabled, err := svc.ds.EnqueueSetupExperienceItems(ctx, host.PlatformLike, hostUUID, teamID)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "check for software titles for setup experience")
	}

	return &fleet.SetupExperienceInitResult{
		Enabled: enabled,
	}, nil
}
